package diff.viewer;

import java.awt.Rectangle;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.util.HashMap;
import javax.swing.JEditorPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import diff.BuiltInDiffProvider;
import diff.Difference;
import diff.HuntDiff;

/**
 * @author WL
 */
public class DiffPaneManager
{
    private DiffPane diffPane;
    private DecoratedDifference[] decoratedDiffArr;
    private BuiltInDiffProvider.Options options = new BuiltInDiffProvider.Options();
    
    private ScrollMapCached scrollMap = new ScrollMapCached();
    private int diffSerial;
    private static final Rectangle INVALID_RECT = new Rectangle(0, 0, 0, 0);
    
    public void setDiffPane(DiffPane diffPane)
    {
        this.diffPane = diffPane;
        
        addScrollListener();
    }
    
    public DiffPane getDiffPane()
    {
        return diffPane;
    }
    
    public void updateDifference()
    {
        updateDifference(diffPane.getEditorPane1().getText(), diffPane.getEditorPane2().getText());
    }
    
    public void updateDifference(String oldText, String newText)
    {
        Difference[] diff = HuntDiff.diff(oldText.split("\n"), newText.split("\n"), options);
        
        decoratedDiffArr = new DecoratedDifference[diff.length];
        generateDecorated(diff, decoratedDiffArr);
        
        updateEditors(diff);
        
        diffSerial++;
    }
    
    private void updateEditors(Difference[] diff)
    {
        ((DiffEditor)diffPane.getEditorPane1()).setDifference(diff);
        ((DiffEditor)diffPane.getEditorPane2()).setDifference(diff);
    }
    
    public DecoratedDifference[] getDecorations()
    {
        return decoratedDiffArr;
    }
    
    public static int getY(int index, JEditorPane editor)
    {
        if (index < 0)
            return 0;
        
        boolean pastEnd = index >= editor.getDocument().getDefaultRootElement().getElementCount();
        Element element = editor.getDocument().getDefaultRootElement().getElement(pastEnd ? index - 1 : index);
        if (element == null)
            return -100;
        int startOffset = element.getStartOffset();
        try {
            Rectangle rec1 = editor.modelToView(startOffset);
            return rec1.y + (pastEnd ? rec1.height : 0);
        } catch (BadLocationException exp) {
            exp.printStackTrace();
            return -100;
        }
    }
    
    private Rectangle getRectForView(int index, boolean isLeft)
    {
        JEditorPane pane = isLeft ? diffPane.getEditorPane1() : diffPane.getEditorPane2();
        
        boolean pastEnd = index >= pane.getDocument().getDefaultRootElement().getElementCount();
        Element element = pane.getDocument().getDefaultRootElement().getElement(pastEnd ? index - 1 : index);
        
        if (element == null)
            return INVALID_RECT;
        
        int offset = element.getStartOffset();
            
        try {
            return pane.modelToView(offset);
        } catch (BadLocationException e) {
            e.printStackTrace();
            return INVALID_RECT;
        }
    }
    
    private int getY(int index, boolean isLeft)
    {
        JEditorPane pane = isLeft ? diffPane.getEditorPane1() : diffPane.getEditorPane2();
        return getY(index, pane);
    }
    
    private void generateDecorated(Difference[] diffs, DecoratedDifference[] decorations)
    {
        for (int i = 0; i < diffs.length; i++) 
        {
            Difference difference = diffs[i];
            DecoratedDifference dd = new DecoratedDifference(difference);
            
            if (difference.getType() == Difference.ADD) {
                dd.topRight = getY(diffs[i].getSecondStart() - 1, false);
                dd.bottomRight = getY(diffs[i].getSecondEnd(), false);
                dd.topLeft = getY(diffs[i].getFirstStart(), true);
                dd.floodFill = true;
            } else if (difference.getType() == Difference.DELETE) {
                dd.topLeft = getY(diffs[i].getFirstStart() - 1, true);
                dd.bottomLeft = getY(diffs[i].getFirstEnd(), true);
                dd.topRight = getY(diffs[i].getSecondStart(), false);
                dd.floodFill = true;
            } else {
                dd.topRight = getY(diffs[i].getSecondStart() - 1, false);
                dd.bottomRight = getY(diffs[i].getSecondEnd(), false);
                dd.topLeft = getY(diffs[i].getFirstStart() - 1, true);
                dd.bottomLeft = getY(diffs[i].getFirstEnd(), true);
                dd.floodFill = true;
            }
            
            decorations[i] = dd;
        }
        decoratedDiffArr = decorations;
    }
    
    private void addScrollListener()
    {
        diffPane.getScrollPane2().getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener(){
            @Override
            public void adjustmentValueChanged(AdjustmentEvent e) {
                smartScroll();
            }
        });
    }
    
    /**
     * 1. find the difference whose top (first line) is closest to the center of the screen. If there is no difference on screen, proceed to #5
     * 2. find line offset of the found difference in the other document
     * 3. scroll the other document so that the difference starts on the same visual line
     * 
     * 5. scroll the other document proportionally
     */    
    private void smartScroll() 
    {
        if (getDecorations() == null)
            return;
        
        int [] map = scrollMap.getScrollMap(diffPane.getEditorPane2().getSize().height, diffSerial);
        
        int rightOffset = diffPane.getScrollPane2().getVerticalScrollBar().getValue();
        if (rightOffset >= map.length)
            return;
        diffPane.getScrollPane1().getVerticalScrollBar().setValue(map[rightOffset]);
    }
    
    private class ScrollMapCached 
    {
        private int     rightPanelHeightCached;
        private int []  scrollMapCached;
        private int     diffSerialCached;

        public synchronized int[] getScrollMap(int rightPanelHeight, int diffSerial) {
            if (rightPanelHeight != rightPanelHeightCached || diffSerialCached != diffSerial || scrollMapCached == null) {
                diffSerialCached = diffSerial;
                rightPanelHeightCached = rightPanelHeight;
                scrollMapCached = compute();
            }
            return scrollMapCached;
        }

        private int [] compute() {
            int rightViewportHeight = diffPane.getScrollPane2().getViewport().getViewRect().height; 

            int [] scrollMap = new int[rightPanelHeightCached];

            int lastOffset = 0;
            
            HashMap<Difference, Rectangle[]> positionsPerDiff = new HashMap<Difference, Rectangle[]>(getDecorations().length);

            DecoratedDifference [] diffs = getDecorations();
            int lastDiffIndex = 0;
            for (int rightOffset = 0; rightOffset < rightPanelHeightCached; rightOffset+=5) { // count position for every fifth pix, others are linearly interpolated
                DifferencePosition dpos = null;
                int leftOffset;
                // find diff for every fifth pix
                int candidateIndex = diffs.length == 0 ? -1 : findDifferenceToMatch(rightOffset, rightViewportHeight, diffs, lastDiffIndex);
                if (candidateIndex > -1) {
                    lastDiffIndex = candidateIndex;
                    DecoratedDifference candidate = diffs[candidateIndex];
                    boolean matchStart = candidate.getTopRight() > rightOffset + rightViewportHeight / 2;
                    if (candidate.getDiff().getType() == Difference.DELETE && candidate.getTopRight() < rightOffset + rightViewportHeight * 4 / 5) matchStart = false;
                    if (candidate.getDiff().getType() == Difference.DELETE && candidate == diffs[diffs.length -1]) matchStart = false;
                    dpos = new DifferencePosition(candidate.getDiff(), matchStart);
                }

                if (dpos == null) {
                    leftOffset = lastOffset + rightOffset;
                } else {
                    Difference diff = dpos.getDiff();
                    Rectangle[] positions = positionsPerDiff.get(diff);
                    if (positions == null) {
                        positions = new Rectangle[4];
                        positions[0] = getRectForView(diff.getFirstStart(), true);
                        positions[1] = getRectForView(diff.getFirstEnd(), true);
                        positions[2] = getRectForView(diff.getSecondStart(), false);
                        positions[3] = getRectForView(diff.getSecondEnd(), false);
                        positionsPerDiff.put(diff, positions);
                    }
                    leftOffset = computeLeftOffsetToMatchDifference(dpos, rightOffset, positions);
                    lastOffset = leftOffset - rightOffset;
                }
                // now try to interpolate for next 4 positions
                int maxIndex = Math.min(rightPanelHeightCached - rightOffset, 5);
                for (int i = 0; i <  maxIndex; ++i) {
                    scrollMap[rightOffset + i] = leftOffset + i;
                }
            }
            scrollMap = smooth(scrollMap);
            return scrollMap;
        }

        private int[] smooth(int[] map) {
            int [] newMap = new int [map.length];
            int leftShift = 0;
            float correction = 0.0f;
            for (int i = 0; i < map.length; i++) {
                int leftOffset = map[i];
                int requestedShift = leftOffset - i; 
                if (requestedShift > leftShift) {
                    if (correction > requestedShift - leftShift) correction = requestedShift - leftShift;
                    leftShift += correction;
                    correction += 0.02f;
                } else if (requestedShift < leftShift) {
                    leftShift -= 1;
                } else {
                    correction = 1.0f;
                }
                newMap[i] = i + leftShift;
            }
            return newMap;
        }
    }
    
    private int computeLeftOffsetToMatchDifference(DifferencePosition differenceMatchStart, int rightOffset, Rectangle[] positions) {
        Rectangle leftStartRect = positions[0], leftEndRect = positions[1], rightStartRect = positions[2], rightEndRect = positions[3];
        Difference diff = differenceMatchStart.getDiff();
        boolean matchStart = differenceMatchStart.isStart();
        
        int value;
        int valueSecond;
        if (matchStart) {
            value = leftStartRect.y + leftStartRect.height;        // kde zacina prva, 180
            valueSecond = rightStartRect.y + rightStartRect.height; // kde by zacinala druha, napr. 230
        } else {
            if (diff.getType() == Difference.ADD) {
                value = leftStartRect.y;        // kde zacina prva, 180
                if (rightStartRect.y == 0) {
                    value -= rightStartRect.height;
                }
                valueSecond = rightEndRect.y + rightStartRect.height; // kde by zacinala druha, napr. 230
            } else {
                value = leftEndRect.y + leftEndRect.height;        // kde zacina prva, 180
                if (diff.getType() == Difference.DELETE) {
                    value += leftStartRect.height;
                    valueSecond = rightStartRect.y + rightStartRect.height; // kde by zacinala druha, napr. 230
                } else {
                    valueSecond = rightEndRect.y + rightEndRect.height; // kde by zacinala druha, napr. 230
                }
            }
        }

        // druha je na 400
        int secondOffset = rightOffset - valueSecond;
        
        value += secondOffset;
        if (diff.getType() == Difference.ADD) value += rightStartRect.height;
        if (diff.getType() == Difference.DELETE) value -= leftStartRect.height;
        
        return value;
    }
    
    private int findDifferenceToMatch (int rightOffset, int rightViewportHeight, DecoratedDifference [] diffs, int index) 
    {
        int candidateIndex = -1;
        // start the loop with the last used index, it will speed-up things
        for (; index < diffs.length; ++index) {
            DecoratedDifference dd = diffs[index];
            if (dd.getTopRight() > rightOffset + rightViewportHeight) break;
            if (dd.getBottomRight() != -1) {
                if (dd.getBottomRight() <= rightOffset) continue;
            } else {
                if (dd.getTopRight() <= rightOffset) continue;
            }
            if (candidateIndex > -1) {
                DecoratedDifference candidate = diffs[candidateIndex];
                if (candidate.getDiff().getType() == Difference.DELETE) {
                    candidateIndex = index;
                } else if (candidate.getTopRight() < rightOffset) { 
                    candidateIndex = index;
                } else if (dd.getTopRight() <= rightOffset + rightViewportHeight / 2) { 
                    candidateIndex = index;
                }
            } else {
                candidateIndex = index;
            }
        }
        return candidateIndex;
    }
    
//    private int computeCurrentDifference() {
//        // jViewport == null iff initialization failed
//        if (manager == null || jViewport2 == null) return 0;
//        Rectangle viewRect = jViewport2.getViewRect();
//        int bottom = viewRect.y + viewRect.height * 4 / 5;
//        DiffViewManager.DecoratedDifference [] ddiffs = manager.getDecorations();
//        for (int i = 0; i < ddiffs.length; i++) {
//            int startLine = ddiffs[i].getTopRight();
//            int endLine = ddiffs[i].getBottomRight();
//            if (endLine > bottom || endLine == -1 && startLine > bottom) return Math.max(0, i-1);
//        }
//        return ddiffs.length - 1;
//    }
    
    public static class DifferencePosition {
        
        private Difference  diff;
        private boolean     isStart;

        public DifferencePosition(Difference diff, boolean start) {
            this.diff = diff;
            isStart = start;
        }

        public Difference getDiff() {
            return diff;
        }

        public boolean isStart() {
            return isStart;
        }
    }
}
